﻿using System;
using System.Web;
using Newtonsoft.Json;

namespace FBGraph.Web
{
    /// <summary>Provides access to strongly typed cookie values that are given user- and application- specific prefixes to keep like-named values separated.</summary>
    public class CookieManager
    {
        private IFBGraphHttpContext context;

        /// <summary>Initializes an instance of <see cref="CookieManager" /> using the specified <see cref="IFBGraphHttpContext" />.</summary>
        /// <param name="context">A reference to an <see cref="IFBGraphHttpContext" /> that will provide access to the HTTP request and response cookie collections.</param>
        public CookieManager(IFBGraphHttpContext context)
        {
            this.context = context;
        }

        private CookieKey GetAppSpecificKey(String key)
        {
            return CookieKey.Create(context.ClientID, key);
        }

        /// <summary>Gets the cookie value with the specified <paramref name="key" />.</summary>
        /// <typeparam name="T">The type the value should be deserialized to.</typeparam>
        /// <param name="key">The name of the cookie.</param>
        /// <returns>The value of the cookie deserialized to <typeparamref name="T" />, or the default value of <typeparamref name="T" /> if the cookie does not exist.</returns>
        public T Get<T>(String key)
        {
            var cookie = Get(key);
            if (cookie == null) return default(T);
            else return JsonConvert.DeserializeObject<T>(cookie.Value);
        }

        /// <summary>Gets the cookie value with the specified <paramref name="key" />.</summary>
        /// <param name="key">The name of the cookie.</param>
        public HttpCookie this[String key]
        {
            get { return Get(key); }
        }

        /// <summary>Gets the cookie value with the specified <paramref name="key" />.</summary>
        /// <param name="key">The name of the cookie.</param>
        public HttpCookie Get(String key)
        {
            var cookieKey = GetAppSpecificKey(key);
            return Get(cookieKey);
        }

        private HttpCookie Get(CookieKey key)
        {
            return context.HttpContext.Request.Cookies[(String)key];
        }

        /// <summary>Gets a <see cref="Boolean" /> value representing whether a cookie with the specified <paramref name="key" /> exists.</summary>
        /// <param name="key">The name of the cookie.</param>
        /// <returns><c>true</c> if the cookie exists; otherwise, <c>false</c>.</returns>
        public Boolean Contains(String key)
        {
            var cookieKey = GetAppSpecificKey(key);
            return Contains(cookieKey);
        }

        private Boolean Contains(CookieKey key)
        {
            return Get(key) != null;
        }

        /// <summary>Removes a cookie with the specified <paramref name="key" />.</summary>
        /// <param name="key">The name of the cookie.</param>
        public void Remove(String key)
        {
            var cookieKey = GetAppSpecificKey(key);
            Remove(cookieKey);
        }

        private void Remove(CookieKey key)
        {
            if (Contains(key))
            {
                var cookie = Get(key);
                cookie.Expires = DateTime.Now.AddSeconds(-1);
                Set(key, cookie);
            }
        }

        /// <summary>Sets or updates the value of a cookie with the specified <paramref name="key" />.</summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="key">The name of the cookie.</param>
        /// <param name="value">The value of the cookie.</param>
        /// <remarks>Complex types will be serialized to JSON.</remarks>
        public void Set<T>(String key, T value)
        {
            Set(key, value, null);
        }

        /// <summary>Sets or updates the value of a cookie with the specified <paramref name="key" />.</summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="key">The name of the cookie.</param>
        /// <param name="value">The value of the cookie.</param>
        /// <param name="expires">The expiration date/time of the cookie. If <paramref name="expires" /> is a null reference, no expiration will be set.</param>
        /// <remarks>Complex types will be serialized to JSON.</remarks>
        public void Set<T>(String key, T value, DateTime? expires)
        {
            var cookie = new HttpCookie(key, JsonConvert.SerializeObject(value));
            if (expires.HasValue) cookie.Expires = expires.Value;

            Set(cookie);
        }

        /// <summary>Sets or updates the value of a cookie with the specified <paramref name="key" />.</summary>
        /// <param name="cookie">An <see cref="HttpCookie" /> to set.</param>
        /// <remarks>Complex types will be serialized to JSON.</remarks>
        public void Set(HttpCookie cookie)
        {
            CookieKey cookieKey = default(CookieKey);
            if (CookieKey.IsCookieKey(cookie.Name)) cookieKey = CookieKey.Import(cookie.Name);
            else cookieKey = CookieKey.Create(context.ClientID, cookie.Name);

            Set(cookieKey, cookie);
        }
        private void Set(CookieKey key, HttpCookie cookie)
        {
            if (cookie.Name != (String)key)
            {
                cookie.Name = (String)key;
            }

            context.HttpContext.Response.SetCookie(cookie);
        }

        private struct CookieKey
        {
            private const Char KEY_SEPARATOR = '~';
            private const String KEY_BASE = "FBGraph";
            private static readonly String KEY_FORMAT = String.Format("{0}{1}{{0}}{1}{1}{{1}}", KEY_BASE, KEY_SEPARATOR);
            private static readonly String[] FormatParts = KEY_FORMAT.Split(KEY_SEPARATOR);

            public static CookieKey Create(Int64 clientID, String key)
            {
                return new CookieKey
                {
                    OriginalKey = key,
                    Value = String.Format(KEY_FORMAT, clientID, key)
                };
            }

            public static CookieKey Import(String cookieKey)
            {
                var keyParts = cookieKey.Split(KEY_SEPARATOR);
                return new CookieKey
                {
                    OriginalKey = keyParts[3],
                    Value = cookieKey
                };
            }

            public String OriginalKey { get; private set; }
            public String Value { get; private set; }

            public override String ToString()
            {
                return this.Value;
            }

            public static explicit operator String(CookieKey key)
            {
                return key.Value;
            }

            public static Boolean IsCookieKey(String key)
            {
                var keyParts = key.Split(KEY_SEPARATOR);
                if (keyParts.Length != FormatParts.Length) return false;
                else return keyParts[0] == KEY_BASE && keyParts[2] == String.Empty;
            }
        }
    }
}